﻿#region Copyright
/// <copyright>
/// Copyright (c) 2008 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

using Osh.Configuration;
using Osh.Media;
using Osh.ViewRight;
using Newtonsoft.Json;

namespace Osh.Qwerty
{
	public class QwertyClient : IMediaContentProvider
	{
		private static Guid MainMediaTreeId = new Guid("{BA177CB3-F0AE-446C-9324-13B2B99FE1D8}");
		private static string MainMediaTreeName = "Все каналы";

		private string _name;
		private ViewRightApplication _player;
		private ChannelDatabase _channelDatabase;
		private string _channelDatabasePath;
		private Uri _serviceUrlBase;
		private CookieContainer _cookies = new CookieContainer(100);
		private DateTime _nextMediaTreeUpdate = DateTime.UtcNow;
		private TimeSpan _normalMediaTreeUpdateInterval = TimeSpan.FromMinutes(60);
		private TimeSpan _failedMediaTreeUpdateInterval = TimeSpan.FromMinutes(10);
		private TimeSpan _epgUpdateInterval = TimeSpan.FromMinutes(10);
		private TimeSpan _timerInterval = TimeSpan.FromSeconds(2);
		private Timer _timer;
		private bool _insideOnTimer;
		private bool _loggedIn = false;
		private EpgParser _epgParser;
		private Regex _authErrorRegex = new Regex("<title>[^<]*ошибки</title>.*>Ошибка авторизации",
			RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant);

		public QwertyClient(string name, ViewRightApplication player)
		{
			_name = name;
			_player = player;
			_epgParser = new EpgParser(this);

			_serviceUrlBase = GetServiceUrlBase();
			Log.Info(TraceSource, "Qwerty service URL: {0}.", _serviceUrlBase);

			_channelDatabasePath = Path.Combine(Config.Current.LocalApplicationDataFolder, "Qwerty\\Channels.xml");
			_channelDatabase = LoadChannelTree();

			player.Services.RegisterService(typeof(QwertyClient), this);
			player.Services.RegisterService(typeof(IMediaLibrary), _channelDatabase);
			OshHostBase.Current.Osh.MediaController.RegisterMediaLibrary(_channelDatabase);

			_timer = new Timer(new TimerCallback(OnTimer), null,
				(int)_timerInterval.TotalMilliseconds, (int)_timerInterval.TotalMilliseconds);
		}

		private void OnTimer(object state)
		{
			if(_insideOnTimer)
				return;

			try
			{
				_insideOnTimer = true;
				if(_nextMediaTreeUpdate < DateTime.UtcNow)
				{
					_nextMediaTreeUpdate = DateTime.UtcNow.Add(_failedMediaTreeUpdateInterval);
					string channelsPage = DownloadChannelTree();
					if(ParseChannelTree(channelsPage))
						_nextMediaTreeUpdate = DateTime.UtcNow.Add(_normalMediaTreeUpdateInterval);
				}
			}
			catch(Exception e)
			{
				Log.Exception(TraceSource, e);
			}
			finally
			{
				_insideOnTimer = false;
			}
		}

		internal ViewRightApplication Player
		{
			get { return _player; }
		}

		internal Uri ServiceUrlBase
		{
			get { return _serviceUrlBase; }
		}

		internal EpgParser EpgParser
		{
			get { return _epgParser; }
		}

		internal ChannelDatabase ChannelDatabase
		{
			get { return _channelDatabase; }
		}

		private ChannelDatabase LoadChannelTree()
		{
			ChannelDatabase channelDatabase = new ChannelDatabase(this);
			try
			{
				if(File.Exists(_channelDatabasePath))
					channelDatabase.Load(_channelDatabasePath);
			}
			catch(Exception e)
			{
				Log.Exception(TraceSource, e, "Cannot load channel database from '{0}'.", _channelDatabasePath);
				throw;
			}
			return channelDatabase;
		}

		private bool ParseChannelTree(string channelTreePageHtml)
		{
			ChannelDatabase newDatabase = new ChannelDatabase(this);

			List<IMediaFolder> folders = new List<IMediaFolder>();
			Tree mediaTree = new Tree(newDatabase, MainMediaTreeId, MainMediaTreeName, folders);
			ParseChannelFolders(channelTreePageHtml, mediaTree, folders);
			
			Dictionary<int,IMediaFolder> idFolderMap = new Dictionary<int,IMediaFolder>();
			foreach(Folder folder in folders)
				idFolderMap[folder.ServerId.Value] = folder;

			ParseChannelList(newDatabase, channelTreePageHtml);

			Log.Info(TraceSource, "Parsed {0} channels and {1} folders.", newDatabase.Channels.Count, folders.Count);
			if(newDatabase.Channels.Count == 0 || folders.Count == 0)
				return false;

			// Distribute channel by folders.
			int numberOfLostChannels = 0;
			foreach(Channel channel in newDatabase.Channels)
			{
				Folder folder = (Folder)idFolderMap[channel.FolderId.Value];
				if(folder != null)
					folder.Channels.Add(channel);
				else
				{
					numberOfLostChannels++;
					Log.Warning(TraceSource, "Folder with id={0} for '{1}' channel not found.", channel.FolderId, channel.Name);
				}
			}

			// Remove empty folders.
			int numberOfEmptyFolders = 0;
			for(int i = folders.Count - 1; i >= 0; i--)
			{
				if(folders[i].Items.Count == 0)
				{
					Log.Warning(TraceSource, "Folder '{0}' is empty and will be removed from the channel tree.", folders[i].Name);
					folders.RemoveAt(i); // Remove folder from media tree.
					numberOfEmptyFolders++;
				}
			}

			if(numberOfLostChannels > 0 || numberOfEmptyFolders > 0)
				Log.Info(TraceSource, "Lost channels: {0}. Empty folders: {1}.", numberOfLostChannels, numberOfEmptyFolders);


			if(newDatabase.Channels.Count == numberOfLostChannels || folders.Count == 0)
				return false;

			newDatabase.Trees.Add(mediaTree);

			_channelDatabase.Update(newDatabase);
			_channelDatabase.Save(_channelDatabasePath);
			return true;
		}

		private void ParseChannelFolders(string channelTreePageHtml, IMediaTree mediaTree, List<IMediaFolder> folders)
		{
			try
			{
				Match match = Regex.Match(channelTreePageHtml, @"var\s+nodeArray\s*=\s*\[");
				if(match.Success)
				{
					int position = match.Groups[0].Index + match.Groups[0].Length - 1; // Start position is an array start character - '['.
					using(JsonReader reader = new JsonReader(new StringReader(channelTreePageHtml.Substring(position))))
					{
						while(reader.Read())
						{
							if(reader.TokenType == JsonToken.StartObject)
							{
								try
								{
									MediaFolder folder = CreateFolderObject(reader, mediaTree);
									if(folder != null)
										folders.Add(folder);
								}
								catch(Exception e)
								{
									Log.Exception(TraceSource, e, "Cannot parse {0} folder.", folders.Count + 1);
									throw e;
								}
							}
						}
					}
				}
			}
			catch(Exception)
			{
				// After the whole array is parsed there will an exception at ')' character.
			}
		}

		private void ParseChannelList(ChannelDatabase database, string channelTreePageHtml)
		{
			try
			{
				Match match = Regex.Match(channelTreePageHtml, @"var\s+channels.*\(\s*\[");
				if(match.Success)
				{
					int position = match.Groups[0].Index + match.Groups[0].Length - 1; // Start position is an array start character - '['.
					using(JsonReader reader = new JsonReader(new StringReader(channelTreePageHtml.Substring(position))))
					{
						while(reader.Read())
						{
							if(reader.TokenType == JsonToken.StartObject)
							{
								try
								{
									database.Channels.Add(CreateChannelObject(reader));
								}
								catch(Exception e)
								{
									Log.Exception(TraceSource, e, "Cannot parse {0} channel.", database.Channels.Count + 1);
									throw e;
								}
							}
						}
					}
				}
			}
			catch(Exception)
			{
				// After the whole array is parsed there will an exception at ')' character.
			}
		}

		private MediaFolder CreateFolderObject(JsonReader reader, IMediaTree mediaTree)
		{
			Dictionary<string, object> properties = ReadObject(reader);

			if((properties["type"] as string) != "category")
				return null;

			int id = Int32.Parse(properties["id"].ToString());
			int parentId = Int32.Parse(properties["parentId"].ToString());
			string name = (string)properties["title"];
			return new Folder(mediaTree, Guid.NewGuid(), id, name);
		}

		private Channel CreateChannelObject(JsonReader reader)
		{
			Dictionary<string, object> properties = ReadObject(reader);

			string logoId = (string)properties["pic"];
			int id = Int32.Parse(properties["id"].ToString());
			int folderId = Int32.Parse(properties["categoryId"].ToString());
			return new Channel(_channelDatabase, Guid.NewGuid(), new Uri((string)properties["filmUrl"]), id, folderId,
				(string)properties["name"], (string)properties["description"], logoId, null, null, null);
		}

		internal Uri CreateChannelLogoUrl(string logoId)
		{
			return new Uri(_serviceUrlBase, "/pic/" + logoId);
		}

		private Dictionary<string, object> ReadObject(JsonReader reader)
		{
			Dictionary<string, object> properties = new Dictionary<string, object>();
			string propertyName = null;
			while(reader.Read())
			{
				if(reader.TokenType == JsonToken.EndObject)
					break;

				if(reader.TokenType != JsonToken.PropertyName)
					break;
				propertyName = (string)reader.Value;

				reader.Read();
				object value = reader.Value;
				properties[propertyName] = value;
			}
			return properties;
		}

		private string DownloadChannelTree()
		{
			Log.Info(TraceSource, "Downloading channel tree.");

			UriBuilder ub = new UriBuilder(this.ServiceUrlBase);
			ub.Path = "/vmplayer/ServiceHandlerAction.do";
			ub.Query = string.Format("serviceCode=1");

			return GetPage(ub.Uri);
		}

		public string GetPage(Uri url)
		{
			if(!EnsureLoggedIn())
				return null;

			string response = GetPageInternal(url);
			// Checks if the reponse has an athorization prompt.
			if(_authErrorRegex.IsMatch(response))
			{
				_loggedIn = false;
				if(!EnsureLoggedIn())
					return null;
				
				response = GetPageInternal(url);
			}

			return response;
		}

		private string GetPageInternal(Uri url)
		{
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
			request.Timeout = 2000;
			request.CookieContainer = _cookies;

			using(HttpWebResponse response = (HttpWebResponse)request.GetResponse())
			{
				StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding(response.CharacterSet));
				
				_cookies.Add(response.Cookies);

				return reader.ReadToEnd();
			}
		}

		private bool EnsureLoggedIn()
		{
			if(_loggedIn)
				return true;

			UriBuilder ub = new UriBuilder(this.ServiceUrlBase);
			ub.Path = "/vmplayer/VMP_LoginAction.do";
			ub.Query = string.Format("MAC={0}&SerialNumber={1}&IP=213.85.187.20",
				Uri.EscapeDataString(_player.MacAddress),
				Uri.EscapeDataString(_player.ClientId));

			string loginPage = GetPageInternal(ub.Uri);

			if(loginPage.IndexOf("VMP_LoginAction", StringComparison.InvariantCultureIgnoreCase) != -1)
				return false;

			_loggedIn = true;
			return true;
		}

		private Uri GetServiceUrlBase()
		{
			string text = ReadPageWithServerIP();
			Match hostMatch = Regex.Match(text, @"host.*:.*""([a-zA-Z0-9\.\-]*)"".*,");
			Match portMatch = Regex.Match(text, @"port.*:.*""+(\d{2,5})""+.*,");
			if(hostMatch.Success && portMatch.Success)
			{
				UriBuilder ub = new UriBuilder(Uri.UriSchemeHttp,
					hostMatch.Groups[1].Value, Int32.Parse(portMatch.Groups[1].Value));
				return ub.Uri;
			}
			return null;
		}

		private string ReadPageWithServerIP()
		{
			if(string.IsNullOrEmpty(_player.InstallDir))
				return string.Empty;

			try
			{
				string path = Path.Combine(_player.InstallDir, "server.js");
				return File.ReadAllText(path, Encoding.ASCII);
			}
			catch(Exception e)
			{
				Log.Exception(TraceSource, e);
				return string.Empty;
			}
		}

		private TraceSource TraceSource
		{
			get { return ViewRightApplication.TraceSource; }
		}

		#region IMediaContentProvider
		public string Name
		{
			get { return _name; }
		}
		#endregion IMediaContentProvider
	}
}
